/*
 * SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
 * SPDX-License-Identifier: GPL-3.0-only
 */

package com.sanji.audio_test

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.util.Log
import androidx.annotation.StringRes
import androidx.core.net.toFile
import com.sanji.audio_test.extension.formattedString
import com.sanji.audio_test.output.OutputFile
import com.sanji.audio_test.settings.SettingsActivity

/**
 * 通知管理类
 * 负责创建和管理应用的各种通知
 */
class Notifications(
    private val context: Context,
) {
    companion object {
        private val TAG = Notifications::class.java.simpleName

        // 持久通知通道ID
        const val CHANNEL_ID_PERSISTENT = "persistent"
        // 失败通知通道ID
        const val CHANNEL_ID_FAILURE = "failure"
        // 成功通知通道ID
        const val CHANNEL_ID_SUCCESS = "success"
        // 静音警告通知通道ID
        const val CHANNEL_ID_SILENCE = "silence"

        // 遗留通知通道ID列表
        private val LEGACY_CHANNEL_IDS = arrayOf("alerts")

        /** 用于访问系统/内部资源值。 */
        private val systemRes = Resources.getSystem()

        /**
         * 硬编码的备用振动模式。
         *
         * 这与AOSP在VibratorHelper（新版本）或NotificationManagerService（旧版本）中定义的相同。
         * 在实践中，除非OEM完全移除了config_defaultNotificationVibePattern数组资源，否则不会使用此模式。
         */
        private val DEFAULT_VIBRATE_PATTERN = longArrayOf(0, 250, 250, 250)

        /** 将资源整数数组转换为长整数数组。 */
        @Suppress("SameParameterValue")
        private fun getLongArray(resources: Resources, resId: Int): LongArray {
            val array = resources.getIntArray(resId)
            val result = LongArray(array.size)
            for (i in array.indices) {
                result[i] = array[i].toLong()
            }
            return result
        }

        /**
         * 从系统内部资源获取默认通知模式。
         *
         * 这是通知默认使用的振动模式。
         */
        @SuppressLint("DiscouragedApi")
        private val defaultPattern = try {
            getLongArray(systemRes, systemRes.getIdentifier(
                "config_defaultNotificationVibePattern", "array", "android"))
        } catch (e: Exception) {
            Log.w(TAG, "未找到系统振动模式; 使用硬编码默认值", e)
            DEFAULT_VIBRATE_PATTERN
        }
    }

    // 应用偏好设置
    private val prefs = Preferences(context)
    // 系统通知管理器
    private val notificationManager = context.getSystemService(NotificationManager::class.java)

    /**
     * 为持久通知创建高优先级通知通道
     */
    private fun createPersistentChannel() = NotificationChannel(
        CHANNEL_ID_PERSISTENT,
        "Background services",
        NotificationManager.IMPORTANCE_HIGH,
    ).apply {
        description = "Persistent notification for background call recording"
    }

    /**
     * 为失败警报创建高优先级通知通道
     */
    private fun createFailureAlertsChannel() = NotificationChannel(
        CHANNEL_ID_FAILURE,
        "Failure alerts",
        NotificationManager.IMPORTANCE_HIGH,
    ).apply {
        description = "Alerts for errors during call recording"
    }

    /**
     * 为成功警报创建高优先级通知通道
     */
    private fun createSuccessAlertsChannel() = NotificationChannel(
        CHANNEL_ID_SUCCESS,
        "Success alerts",
        NotificationManager.IMPORTANCE_HIGH,
    ).apply {
        description = "Alerts for successful call recordings"
    }

    /**
     * 为纯静音警告创建高优先级通知通道
     */
    private fun createPureSilenceWarningsChannel() = NotificationChannel(
        CHANNEL_ID_SILENCE,
        "Pure silence warnings",
        NotificationManager.IMPORTANCE_HIGH,
    ).apply {
        description = "Warn if call recordings contained pure silence"
    }

    /**
     * 确保通知通道是最新的。
     *
     * 遗留通知通道将被删除而不迁移设置。
     */
    fun updateChannels() {
        notificationManager.createNotificationChannels(listOf(
            createPersistentChannel(),
            createFailureAlertsChannel(),
            createSuccessAlertsChannel(),
            createPureSilenceWarningsChannel(),
        ))
        LEGACY_CHANNEL_IDS.forEach { notificationManager.deleteNotificationChannel(it) }
    }

    /**
     * 创建用于录制过程中的持久通知。通知外观是完全静态的，
     * 正在进行的录制通过通知的存在或不存在来表示。
     * 
     * @param titleResId 通知标题资源ID
     * @param message 通知消息内容
     * @param actions 通知操作列表
     * @return 构建好的通知对象
     */
    fun createPersistentNotification(
        @StringRes titleResId: Int,
        message: String?,
        actions: List<Pair<Int, Intent>>,
    ): Notification {
        val notificationIntent = Intent(context, SettingsActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(
            context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
        )

        return Notification.Builder(context, CHANNEL_ID_PERSISTENT).run {
            setContentTitle(context.getText(titleResId))
            if (message != null) {
                setContentText(message)
                style = Notification.BigTextStyle()
            }
            setSmallIcon(android.R.drawable.ic_menu_manage)
            setContentIntent(pendingIntent)
            setOngoing(true)
            setOnlyAlertOnce(true)

            for ((actionTextResId, actionIntent) in actions) {
                val actionPendingIntent = PendingIntent.getService(
                    context,
                    0,
                    actionIntent,
                    PendingIntent.FLAG_IMMUTABLE or
                            PendingIntent.FLAG_UPDATE_CURRENT or
                            PendingIntent.FLAG_ONE_SHOT,
                )

                addAction(Notification.Action.Builder(
                    null,
                    context.getString(actionTextResId),
                    actionPendingIntent,
                ).build())
            }

            // 抑制显示持久通知时的10秒延迟
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
            }
            build()
        }
    }

    /** 检查文件是否存储在设备保护存储中。 */
    private fun isDeviceProtectedStorage(outputFile: OutputFile): Boolean {
        if (outputFile.uri.scheme != ContentResolver.SCHEME_FILE) {
            return false
        }

        val file = outputFile.uri.toFile()

        return file.startsWith(prefs.directBootInProgressDir)
                || file.startsWith(prefs.directBootCompletedDir)
    }

    /**
     * 发送带有给定标题的录制警报通知。
     *
     * * 如果[errorMsg]不为空，则将其前置到文本中，并在其后添加空行。
     * * 如果[file]不为空，则将人类可读的URI路径附加到文本中，如果需要则在前面添加空行。
     *   此外，通知中会添加三个操作：打开/分享/删除。删除操作会关闭通知，但打开和分享不会。
     *   点击通知本身的行为类似于打开操作，但通知会被关闭。
     *   但是，如果文件位于设备保护存储中，则会移除所有操作，因为用户无法访问该文件。
     * 
     * @param channel 通知通道ID
     * @param title 通知标题资源ID
     * @param errorMsg 错误消息
     * @param file 主要输出文件
     * @param additionalFiles 附加文件列表
     */
    private fun sendRecordingNotification(
        channel: String,
        @StringRes title: Int,
        errorMsg: String?,
        file: OutputFile?,
        additionalFiles: List<OutputFile>,
    ) {
        val notificationId = prefs.nextNotificationId

        val notification = Notification.Builder(context, channel).run {
            val text = buildString {
                val errorMsgTrimmed = errorMsg?.trim()
                if (!errorMsgTrimmed.isNullOrBlank()) {
                    append(errorMsgTrimmed)
                }
                if (file != null) {
                    if (isNotEmpty()) {
                        append("\n\n")
                    }
                    append(file.uri.formattedString)
                }
            }

            setContentTitle(context.getString(title))
            if (text.isNotBlank()) {
                setContentText(text)
                style = Notification.BigTextStyle()
            }
            setSmallIcon(android.R.drawable.ic_dialog_info)

            if (file != null && !isDeviceProtectedStorage(file)) {
                // 无法授予其他应用程序对SAF URI的访问权限
                val wrappedUri = RecorderProvider.fromOrigUri(file.uri)

                val openIntent = PendingIntent.getActivity(
                    context,
                    0,
                    Intent(Intent.ACTION_VIEW).apply {
                        setDataAndType(wrappedUri, file.mimeType)
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    },
                    PendingIntent.FLAG_IMMUTABLE,
                )
                val shareIntent = PendingIntent.getActivity(
                    context,
                    0,
                    Intent(Intent.ACTION_SEND).apply {
                        // 数据不用于ACTION_SEND，但它使Intent.filterEquals()
                        // 返回false，并防止在显示多个通知时使用相同的PendingIntent。
                        setDataAndType(wrappedUri, file.mimeType)
                        putExtra(Intent.EXTRA_STREAM, wrappedUri)
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    },
                    PendingIntent.FLAG_IMMUTABLE,
                )
                val deleteIntent = PendingIntent.getService(
                    context,
                    0,
                    NotificationActionService.createDeleteUriIntent(
                        context,
                        listOf(file) + additionalFiles,
                        notificationId,
                    ),
                    PendingIntent.FLAG_IMMUTABLE,
                )

                addAction(Notification.Action.Builder(
                    null,
                    "Open",
                    openIntent,
                ).build())

                addAction(Notification.Action.Builder(
                    null,
                    "Share",
                    shareIntent,
                ).build())

                addAction(Notification.Action.Builder(
                    null,
                    "Delete",
                    deleteIntent,
                ).build())

                // 点击通知的行为类似于打开操作，但通知会被关闭。
                // 打开和分享操作不会关闭通知。
                setContentIntent(openIntent)
                setAutoCancel(true)
            }

            build()
        }

        notificationManager.notify(notificationId, notification)
    }

    /**
     * 发送录制成功警报通知。
     *
     * 如果用户为[CHANNEL_ID_SUCCESS]通道启用了振动，这将明确地使设备振动。
     * 这是必要的，因为Android本身在通话期间不会为通知产生振动。
     *
     * 如果[file]位于设备保护存储中，则不会发送通知，因为用户无法对其进行任何有意义的操作。
     * 
     * @param file 主要输出文件
     * @param additionalFiles 附加文件列表
     */
    fun notifyRecordingSuccess(file: OutputFile, additionalFiles: List<OutputFile>) {
        if (isDeviceProtectedStorage(file)) {
            return
        }
        sendRecordingNotification(
            CHANNEL_ID_SUCCESS,
            "Successfully recorded call",
            null,
            file,
            additionalFiles,
        )
        vibrateIfEnabled(CHANNEL_ID_SUCCESS)
    }

    /**
     * 发送录制失败警报通知。
     *
     * 如果用户为[CHANNEL_ID_FAILURE]通道启用了振动，这将明确地使设备振动。
     * 这是必要的，因为Android本身在通话期间不会为通知产生振动。
     *
     * 如果[file]位于设备保护存储中，通知仍会显示，但没有任何操作。
     * 
     * @param errorMsg 错误消息
     * @param file 主要输出文件（可为空）
     * @param additionalFiles 附加文件列表
     */
    fun notifyRecordingFailure(
        errorMsg: String?,
        file: OutputFile?,
        additionalFiles: List<OutputFile>,
    ) {
        sendRecordingNotification(
            CHANNEL_ID_FAILURE,
            "Failed to record call",
            errorMsg,
            file,
            additionalFiles,
        )
        vibrateIfEnabled(CHANNEL_ID_FAILURE)
    }

    /**
     * 发送纯静音警告通知。
     *
     * 如果用户为[CHANNEL_ID_SILENCE]通道启用了振动，这将明确地使设备振动。
     * 这是必要的，因为Android本身在通话期间不会为通知产生振动。
     * 
     * @param packageName 应用包名
     */
    fun notifyRecordingPureSilence(packageName: String) {
        sendRecordingNotification(
            CHANNEL_ID_SILENCE,
            "Failed to record call",
            context.getString("The recording was discarded because the audio was completely silent. This device might not support recording calls associated with the \'%s\' app.", packageName),
            null,
            emptyList(),
        )
        vibrateIfEnabled(CHANNEL_ID_SILENCE)
    }

    /** 发送直接启动模式文件迁移失败警报通知。 */
    fun notifyMigrationFailure(errorMsg: String?) {
        val notificationId = prefs.nextNotificationId

        val notification = Notification.Builder(context, CHANNEL_ID_FAILURE).run {
            val text = errorMsg?.trim() ?: ""

            setContentTitle("Failed to migrate recordings")
            if (text.isNotBlank()) {
                setContentText(text)
                style = Notification.BigTextStyle()
            }
            setSmallIcon(android.R.drawable.ic_menu_manage)

            build()
        }

        notificationManager.notify(notificationId, notification)
    }

    /** 关闭所有警报（非持久）通知。 */
    fun dismissAll() {
        for (notification in notificationManager.activeNotifications) {
            if (notification.isClearable) {
                notificationManager.cancel(notification.tag, notification.id)
            }
        }
    }

    /**
     * 如果用户为[channelId]通道启用了振动，则明确地使设备振动。
     *
     * 如果通知通道有关联的特定振动模式，则将使用该振动模式。
     * 否则，此函数会尽可能模仿系统振动模式。
     * 系统默认振动模式从Android的内部资源中查询，如果查询失败则回退到硬编码默认值（与AOSP相同）。
     *
     * 此函数不尝试播放PWLE波形。该API在Android 13发布中未稳定，
     * 而且目前没有设备支持，因此不值得使用反射。
     *
     * 理想情况下，使用NotificationRecord.getVibration()是确保振动模式与系统生成的模式相同的最佳方式，
     * 但该类在常规应用程序的类路径中不可用。
     * 
     * @param channelId 通知通道ID
     */
    fun vibrateIfEnabled(channelId: String) {
        val channel = notificationManager.getNotificationChannel(channelId)
        if (channel.shouldVibrate()) {
            val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                val vibratorManager = context.getSystemService(VibratorManager::class.java)
                vibratorManager.defaultVibrator
            } else {
                context.getSystemService(Vibrator::class.java)
            }

            if (vibrator.hasVibrator()) {
                val pattern = channel.vibrationPattern ?: defaultPattern
                val effect = VibrationEffect.createWaveform(pattern, -1)

                vibrator.vibrate(effect)
            }
        }
    }
}